#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# https://code.google.com/p/otfinst/
# Formerly known as otftex_install
#
# Copyright (c) 2005--2006, 2010, 2011, John Owens, Nico Schlömer.
# University of California, Davis. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# Neither the name of the University of California nor the names of
# its contributors may be used to endorse or promote products derived
# from this software without specific prior written permission. THIS
# SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

import sys, os, re, datetime, glob, optparse, subprocess, itertools, operator

# ==============================================================================
# meta information
__author__     = [ "John Owens", "Nico Schlömer" ]
__copyright__  = "Copyright (c) 2005--2006, 2010, 2011, John Owens, Nico Schlömer." \
                 " All rights reserved."
__credits__    = []
__license__    = "BSD"
__version__    = "2.0.0"
__maintainer__ = [ "John Owens", "Nico Schlömer" ]
__email__      = [ 'jowens@ece.ucdavis.edu', 'nico.schloemer@gmail.com' ]
__status__     = "Development"
# ==============================================================================
# global variables
OPTIONS = None
# ==============================================================================
# some LEGACY code:
# which OpenType options do we support in this script?
# add more to option_list if you want the script to support them
OPTION_LIST = [
  # in option_list, each element is itself a list
  # we generate options by always picking one choice from each element
  # and taking all permutations of those choices
  ['kern'],                     # always selected
  ['liga'],
  ['lnum', 'onum', 'swsh', 'sinf', 'sups', 'pnum', 'fakenum'], # one of these
  ['smcp', ''],                 # either smcp or nothing
  ]

# categorize the OpenType options above
# GLOBAL_OPTION_LIST contains options that we should apply to all fonts
GLOBAL_OPTION_LIST = ['kern', 'liga']
# NEW_FONT_FAMILY_OPTION_LIST has options that create new font families
NEW_FONT_FAMILY_OPTION_LIST = ['lnum', 'onum', 'swsh', 'sinf', 'sups', 'pnum',
                           'fakenum']
# variantOptionsList contains options that modify the variant
VARIANT_OPTION_LIST = ['smcp']
# ==============================================================================
def main():
    """
    Main program.
    """
    # This function is called at the very bottom of this file.

    # parse command line options
    args = command_line_options()
    show_options()

    check_lcdf_version()

    # fonthash:
    # indexed by font file name, contains (flat) info about font file:
    # filename, family, subfamily, vendor, optical_size, series, shape, features
    fonthash = []
    for fontfile in find_font_files( args ):
        fonthash.append( get_font(fontfile) )

    encoding_font_combinations = generate_fd_and_sty( fonthash )

    # Make sure that the no TeX font name appears more than once.
    # Otherwise, file names will conflict which will lead to unexpected results.
    assert_unique_tex_font_names( encoding_font_combinations )

    execute_commands( encoding_font_combinations, OPTIONS.dryrun )
    return 0
# ==============================================================================
def command_line_options():
    """
    Defines and handles command line arguments.
    """
    # --------------------------------------------------------------------------
    # create parser
    usage = "usage: %prog [options] font[s]"
    version_string = __name__ + " " + __version__ + "\n" + \
                     __copyright__

    desc = "This is a description."

    epi = "For bug reports or suggestions, contact " + \
          ", ".join(__email__[:-1]) + " or " +  __email__[-1] + " ."

    parser = optparse.OptionParser( usage = usage,
                                    version = version_string,
                                    description = desc,
                                    epilog = epi )
    # --------------------------------------------------------------------------
    # add options
    parser.add_option( "-d", "--dry-run",
                       action = "store_true",
                       dest   = "dryrun",
                       help   = "print what commands would be used, " \
                                "but don't actually execute them" )

    parser.add_option( "-e", "--execute",
                       action  = "store_false",
                       dest    = "dryrun",
                       default = True,
                       help    = "actually execute the otftotfm commands " \
                                 "and place create the font files" )

    parser.add_option( "-n", "--encodings",
                       dest="encodings",
                       default="LY1",
                       help="use encodings ENCLIST, e.g., 'LY1,T1'",
                       metavar="ENCLIST" )

    parser.add_option( "-b", "--berryname",
                       dest    = "berryname",
                       help    = "use three-letter font Berry name BER; this is only used for --short-names (default)",
                       metavar = "BER" )

    parser.add_option( "-y", "--family",
                       dest    = "tex_family",
                       default = 'rm',
                       help    = "install the font as roman (rm, default), " \
                                 "sans serif (sf), or typewriter (tt)",
                       metavar = "FM" )

    parser.add_option( "-x", "--extra",
                       dest    = "extraoptions",
                       help    = "extra options to be passed to otftotfm",
                       metavar = "OPT" )

    parser.add_option( "-s", "--short-names",
                       action = "store_true",
                       dest   = "shortnames",
                       default = True,
                       help   = "use old-style short variable names " \
                                "internally (e.g., pagr8t for Adobe Garamond," \
                                " Regular, Cork encoding)" )

    parser.add_option( "-l", "--long-names",
                       action  = "store_false",
                       dest    = "shortnames",
                       help    = "use long variable names internally (e.g., " \
                                 "Adobe-Garamond-Regular=LF-Cork  pagr8t for Adobe Garamond, Regular, Cork " \
                                 "encoding)" )

    parser.add_option( "-f", "--fallback",
                       dest = "fallback_properties",
                       help = "in case otfinst fails to recognize properties " \
                              "of the font, use the given as a " \
                              "comma-separated list as fallback" )

    parser.add_option( "-v", "--verbose",
                       action = "store_true",
                       dest   = "verbose",
                       default = True,
                       help   = "print status messages to stdout" )

    parser.add_option( "-q", "--quiet",
                       action = "store_false",
                       dest   = "verbose",
                       help   = "don't print status messages to stdout" )
    # --------------------------------------------------------------------------
    # parse and post-process options
    global OPTIONS
    (OPTIONS, arguments) = parser.parse_args()

    if OPTIONS.encodings:
        OPTIONS.encodings = OPTIONS.encodings.split( "," )

    if OPTIONS.fallback_properties:
        OPTIONS.fallback_properties = OPTIONS.fallback_properties.split(",")
    # --------------------------------------------------------------------------
    return arguments
# ==============================================================================
def show_options():
    """
    Prettyprints the options given to otfinst to screen.
    """
    if OPTIONS.verbose:
        print "Using the following otfinst options:"
        print "\tdry run:\t\t",
        if OPTIONS.dryrun:
            print "yes"
        else:
            print "no"
        print
        print "\tEncoding(s):\t\t" + ", ".join( OPTIONS.encodings )
        print

        print "\tTeX names:\t\t",
        if OPTIONS.shortnames:
            print "Berry"
        else:
            print "long"

        print "\tVerbosity:\t\t",
        if OPTIONS.verbose:
            print "verbose"
        else:
            print "quiet"

        print "\textra otfinst options:\t",
        if OPTIONS.extraoptions:
            print OPTIONS.extraoptions
        else:
            print "-"

        print "\tfallback font props:\t",
        if OPTIONS.fallback_properties:
            print  ", ".join( OPTIONS.fallback_properties )
        else:
            print "-"
        print
    return
# ==============================================================================
def prettyprint_font_info( font ):
    """
    Prettyprints some info of a given font object.
    """
    msg = ''
    
    empty_property_string = '-'

    msg += font['file name'] + ":\n"
    msg += "\tFamily:\t\t" + font['family'] + '\n'

    msg +=  "\tWidth:\t\t"
    if font['width'] is not None:
        msg +=  font['width']
    else:
        msg +=  empty_property_string
    msg += '\n'

    msg +=  "\tWeight:\t\t"
    if font['weight'] is not None:
        msg +=  font['weight']
    else:
        msg +=  empty_property_string
    msg += '\n'

    msg +=  "\tShape:\t\t"
    if font['shape'] is not None:
        msg +=  font['shape']
    else:
        msg +=  empty_property_string
    msg += '\n'

    msg +=  "\tVariant:\t"
    if font['variant'] is not None:
        msg +=  font['variant']
    else:
        msg +=  empty_property_string
    msg += '\n'

    msg += "\tOptical size:\t"
    if font['optical size'] is not None:
        msg += str( font['optical size'] )
    else:
        msg += empty_property_string
    msg += '\n'

    msg += "\tFigure style:\t"
    if font['figure style'] is not None:
        msg += str( font['figure style'] )
    else:
        msg += empty_property_string
    msg += '\n'

    msg += "\tFeatures:\t" + ", ".join( font['features'] )

    msg += "\n"

    return msg
# ==============================================================================
def letterize( string ):
    """
    Removes numeral from a string and replaces them by appropriate
    alphabeticals.
    """
    string = string.replace('0', 'Zero')
    string = string.replace('1', 'One')
    string = string.replace('2', 'Two')
    string = string.replace('3', 'Three')
    string = string.replace('4', 'Four')
    string = string.replace('5', 'Five')
    string = string.replace('6', 'Six')
    string = string.replace('7', 'Seven')
    string = string.replace('8', 'Eight')
    string = string.replace('9', 'Nine')

    return string
# ==============================================================================
def get_tex_font_name( font, encoding ):
    """
    TeX-internal name of the font. Depending on whether or not
    'options.shortnames' is true, this is either of type 'pagrj8t' or
    'Adobe-Garamond-Regular=LF-Cork'.
    """
    if OPTIONS.shortnames:
        return get_berry_font_name( font, encoding )
    else:
        return get_long_font_name( font, encoding )
# ==============================================================================
# see the fontinstallationguide, p. 86
# <http://www.ctan.org/tex-archive/info/Type1fonts/fontinstallationguide/>
BERRY_WEIGHT_MAP = { 'ultralight' : 'a',
                     'thin'       : 'a',
                     'hairline'   : 'a',
                     'extralight' : 'j',
                     'light'      : 'l',
                     'book'       : 'k',
                     'regular'    : 'r',
                     'roman'      : 'r',  # not in fontinstallationguide
                     'medium'     : 'm',
                     'demi'       : 'd',  # not in fontinstallationguide
                     'demibold'   : 'd',
                     'semibold'   : 's',
                     'bold'       : 'b',
                     'heavy'      : 'h',
                     'heavyface'  : 'h',  # not in fontinstallationguide
                     'black'      : 'c',
                     'extrabold'  : 'x',
                     'extrablack' : 'x',
                     'ultrabold'  : 'u',
                     'ultrablack' : 'u',
                     'poster'     : 'p'
                   }
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def get_berry_weight( font ):
    """
    Returns the Berry style string of a given font object.
    """
    if font['weight'] is None:
        return 'r'

    if font['weight'] in BERRY_WEIGHT_MAP:
        return BERRY_WEIGHT_MAP[ font['weight'] ]
    else:
        sys.exit( "Font weight '" + str(font['weight']) + "' could not be " \
                  " found in the Berry weight mapping table. Abort." )
# ==============================================================================
def get_berry_variant( font ):
    """
    Constructs the Berry variant fingerprint from a given font.
    TODO Expand this function to reflect more variants.
    """
    variant_list = []

    if font['shape'] == 'oblique' or font['shape'] == 'slanted':
        variant_list.append( 'o' )

    if font['shape'] == 'upright italic':
        variant_list.append( '--' )

    # check italic and small caps after each other to make sure to retain
    # the mandatory component 'ic' in case both is true
    if font['shape'] == 'italic':
        variant_list.append( 'i' )

    if font['variant'] == 'smcp':
        variant_list.append( 'c' )

    if font['variant'] == 'outline':
        variant_list.append( 'l' )
 
    if font['variant'] == 'script':
        variant_list.append( 'w' )

    if font['optical size name'] == 'display':
        variant_list.append( 'd' )

    if font['figure style'] == 'osf':
        variant_list.append( 'j' )

    if variant_list:
        return "".join( variant_list )
    else:
        # Alternatively, one could also return 'r' for regular.
        # This is normally omitted, though.
        return None
# ==============================================================================
def get_berry_encoding( encoding ):
    """
    Returns the Berry encoding string of a given font object.
    """
    try:
        return  ENCODINGS[ encoding ]['berryencoding']
    except KeyError:
        return None
# ==============================================================================
# see the fontinstallationguide, p. 86
# <http://www.ctan.org/tex-archive/info/Type1fonts/fontinstallationguide/>
BERRY_WIDTH_MAP = { 'ultracompressed' : 'u',
                    'ultracondensed'  : 'o',
                    'extracompressed' : 'q',
                    'extracondensed'  : 'q',
                    'compressed'      : 'p',
                    'compact'         : 'p',  # not in fontinstallationguide
                    'condensed'       : 'c',
                    'cond'            : 'c',  # not in fontinstallationguide
                    'narrow'          : 'n',
                    'semicondensed'   : 'n',  # not in fontinstallationguide
                    'normal'          : None, # not in fontinstallationguide
                    'medium'          : None, # not in fontinstallationguide
                    'regular'         : None,
                    None              : None, # not in fontinstallationguide
                    'extended'        : 'x',
                    'elongated'       : 'x',  # not in fontinstallationguide
                    'semiexpanded'    : 'y',  # not in fontinstallationguide
                    'expanded'        : 'e',
                    'extraexpanded'   : 'v',
                    'ultraexpanded'   : '--',
                    'wide'            : 'w'
                  }
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def get_berry_width( font ):
    """
    Returns the Berry width string of a given font object.
    """
    try:
        return BERRY_WIDTH_MAP[ font['width'] ]
    except KeyError:
        sys.exit( "Could not find width '" + str(font['width']) + \
                  "' in width map. Abort." )
# ==============================================================================
def get_berry_opticalsize( font ):
    """
    Reads and returns the optical size of a font.
    """

    if font['optical size'] is not None:
        optical_size = font['optical size']
        # return the integer part of the optical size specification
        if len(optical_size) == 1:
            return "%d" % int(optical_size)
        elif len(optical_size) == 2:
            return "%d" % int(optical_size[0])
        else:
            sys.exit( "Illegal optical size specification '" + \
                      str(optical_size) + "'. Abort." )
    else:
        return None
# ==============================================================================
def get_berry_font_name( font, encoding ):
    """
    Constructs the Berry-style font name.
    See <http://www.tug.org/fontname/fontname.pdf>.

    Extract:
    Here is the basic scheme (the spaces here are merely for readability):
        S TT W [V...] [N] [E] [DD]
    where
    S     represents the supplier of the font.
    TT    represents the typeface name.
    W     represents the weight.
    V...  represents the variant(s), and is omitted if both it and the width
          are normal. Many fonts have more than one variant.
    N     represents the encoding, and is omitted if the encoding is
          nonstandard. Encodings are subsumed in the section on variants
          (see Section 2.4 [Variants], page 20).
    E     represents the width (“expansion”), and is omitted if it is normal.
    DD    represents the design size (in decimal), and is omitted if the font is
          linearly scaled. Mittelbach in TUGboat 13(1) proposes using
          hexadecimal or base-36 notation. In my opinion, the increased range
          does not make up for the unreadability of the common sizes (e.g.,
          10 pt fonts would have a suffix ‘a’ (in hex), or ‘j’ (in base 36)).
    """
    fontname_components = []
    # --------------------------------------------------------------------------
    # mandatory components
    # STT
    fontname_components.append( get_berry_familyname( font['vendor'],
                                                      font['family'] ) )

    # W
    fontname_components.append( get_berry_weight( font ) )
    # --------------------------------------------------------------------------
    # optional components

    # V
    var = get_berry_variant( font )
    if var is not None:
        fontname_components.append( var )

    # N
    enc = get_berry_encoding( encoding )
    if enc is not None:
        fontname_components.append( enc )

    # E
    width = get_berry_width( font )
    if width is not None:
        fontname_components.append( width )

    # DD
    optical_size = get_berry_opticalsize( font )
    if optical_size is not None:
        fontname_components.append( optical_size )
    # --------------------------------------------------------------------------
    return  "".join(fontname_components)
# ==============================================================================
def get_long_font_name( font, encoding ):
    """
    As an alternative to the Berry font naming scheme, the scheme

        supplier-family-weight-variants-width-encoding--size

    was suggested in <http://www.tug.org/fontname/fontname.pdf>. This function
    returns the corresponding string given a 'font' dictionary object.
    """
    # --------------------------------------------------------------------------
    # mandatory components
    fontname_components = [ font['vendor'].capitalize(), font['family'] ]
    # --------------------------------------------------------------------------
    # optional
    if font['weight']:
        fontname_components.append( font['weight'].capitalize() )
    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    # Gather variant keywords, and replace them by a prettier string (e.g.,
    # 'Italics' instead of 'italics', 'SmallCaps' instead of 'smcp').
    variant_list = []
    if font['shape']:
        variant_list.append( font['shape'].capitalize() )
    if font['variant']:
        if font['variant'] == 'smcp':
            variant_list.append( 'SmallCaps' )
        else:
            variant_list.append( font['variant'].capitalize() )
    if font['figure style']:
        if font['figure style'] == 'lf':
            variant_list.append( 'LF' )
        elif font['figure style'] == 'osf':
            variant_list.append( 'OsF' )
        else:
            variant_list.append( font['figure style'].capitalize() )
    if variant_list:
        fontname_components.append( "=".join( variant_list ) )
    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    if font['width']:
        fontname_components.append( font['width'].capitalize() )
    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    fontname_components.append( ENCODINGS[encoding]['name'] )
    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    fontname = "-".join( fontname_components )
    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    # As optical appendix, take the the optical size without decimal point
    # if there is one size, and the first component without decimal point
    # if it is a size range.
    if font['optical size']:
        if len(font['optical size']) == 1:
            os_appendix = str( font['optical size'] ).replace(".","")
        elif len(font['optical size']) == 2:
            os_appendix = str( font['optical size'][0] ).replace(".","")
        else:
            sys.exit( "Illegal 'optical size' entry '" + \
                      str(font['optical size']) + "' found. Abort." )

        fontname += "--" + os_appendix
    # --------------------------------------------------------------------------
    return fontname
# ==============================================================================
def get_raw_tex_familyname( vendor, family ):
    """
    TeX-internal name of the font family. Depending on whether or not
    'options.shortnames' is true, this is either of type 'pag' or
    'Adobe-Garamond'.
    """
    if OPTIONS.shortnames:
        return get_berry_familyname( vendor, family )
    else:
        return family
# ==============================================================================
# To enable TeX to distinguish between figure styles of the same font family,
# several TeX families are created. If OPTIONS.shortnames, they consist of the
# three-letter Berry font family ID, plus an appendix that is defined here.
# In case of old style figures, append a 'j' to the font family name;
# this follows the LaTeX Font Installation Guide,
# <http://www.ctan.org/tex-archive/info/Type1fonts/fontinstallationguide/>,
# p. 40f.
# Similar for long font names.
FIGURESTYLE_APPENDIX_SHORT = { "lf" : "",
                               "osf": "j" }
FIGURESTYLE_APPENDIX_LONG = { "lf" : "-LF",
                              "osf": "-OsF" }
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def get_tex_familyname( vendor, family, figure_style ):
    """
    TeX-internal name of the font family. Depending on whether or not
    'options.shortnames' is true, this is either of type 'pag' or
    'Adobe-Garamond'.
    """
    name = get_raw_tex_familyname( vendor, family )

    if OPTIONS.shortnames:
        name += FIGURESTYLE_APPENDIX_SHORT[ figure_style ]
    else:
        # If the figure style is explicitly given, append something to reflect
        # it.
        name += FIGURESTYLE_APPENDIX_LONG[ figure_style ]

    return name
# ==============================================================================
# See <http://www.tug.org/fontname/fontname.pdf>.
BERRY_VENDOR_CODE = { 'fontfont'    : '0',
                      'elsnerflake' : '2',
                      'softmake'    : '5',
                      'autologi'    : 'a',
                      'bistrea'     : 'b',
                      'cg'          : 'c',
                      'dtc'         : 'd',
                      'apple'       : 'e',
                      'public'      : 'f', # small foundries
                      'gnu'         : 'g',
                      'bh'          : 'h',
                      'itc'         : 'i',
                      'microsoft'   : 'j',
                      'softkey'     : 'k',
                      'linotype'    : 'l',
                      'monotype'    : 'm',
                      'ibm'         : 'n',
                      'corel'       : 'o',
                      'adobe'       : 'p',
                      'texgyre'     : 'q',
                      'sun'         : 's',
                      'paragrap'    : 't',
                      'urw'         : 'u',
                      'sil'         : 'v',
                      'wolfram'     : 'w',
                      'arkandis'    : 'y',
                      'autofont'    : '-',
                      'jknappen'    : '-',
                      'mnm'         : '-',
                      'yandy'       : '-'
                    }

BERRY_FAMILY_CODE = { 'Minion Pro'         : 'mn',
                      'Myriad Pro'         : 'my',
                      'Adobe Garamond Pro' : 'ag',
                      'Adobe Caslon Pro'   : 'ac',
                      'Adobe Jenson Pro'   : 'aj',
                      'Utopia Std'         : 'ut',
                      'Warnock Pro'        : 'wp',
                      'Kepler Std'         : 'kp',
                      'Prestige Elite Std' : 'pe',
                      'Calibri'            : 'k0',
                      'Cambria'            : 'k1',
                      'Candara'            : 'k2',
                      'Consolas'           : 'k3',
                      'Constantia'         : 'k4',
                      'Corbel'             : 'k5',
                      'Arno Pro'           : 'a0',
                      'ScalaSansPro'       : 'sa',
                      'Hypatia Sans Pro'   : 'hy'
                    }
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def get_berry_familyname( vendor, family ):
    """
    Constructs the Berry-style family name of vendor and font.
    See <http://www.tug.org/fontname/fontname.pdf>.
    """
    if OPTIONS.berryname is not None:
        return OPTIONS.berryname
    elif family in BERRY_FAMILY_CODE:
        
        family_abbrev = BERRY_FAMILY_CODE.keys[ family ]
        if vendor in BERRY_VENDOR_CODE:
            vendor_letter = BERRY_VENDOR_CODE[ vendor ]
        else:
            vendor_letter = 'f'

        return vendor_letter + family_abbrev
    else:
        sys.exit( "Could not automatically determine the three-letter font " \
                  " abbreviation for the font '%s' of '%s'.\n" \
                  "Please provide one via the command line option " \
                  "'--berryname'." % ( family, vendor ) )
# ==============================================================================
def find_font_files( filelist ):
    """
    Get all candidate files.
    """
    globlist = []
    fontfiles = []

    # glob the input list
    for item in filelist:
        globlist += glob.glob('%s' % item)

    for item in globlist:
        if os.path.isfile( item ):
            # add files directly to the candidate list
            fontfiles.append(item)
        elif os.path.isdir( item ):
            # add all files in the directory to the candidate list
            for root, dirs, files in os.walk( item ):
                for filename in files:
                    fontfiles.append( os.path.join(root, filename) )

    # filter the files according to is_valid_font_file
    fontfiles = filter( is_valid_font_file, fontfiles )

    if not fontfiles:
        sys.exit( "No files found." )

    return fontfiles
# ==============================================================================
def run( command ):
    """
    Runs a given command on the command line and returns its output.
    """
    if OPTIONS.verbose:
        print command

    # From <http://docs.python.org/library/subprocess.html>:
    # If close_fds is true, all file descriptors except 0, 1 and 2 will be
    # closed before the child process is executed. (Unix only). Or, on Windows,
    # if close_fds is true then no handles will be inherited by the child
    # process. Note that on Windows, you cannot set close_fds to true and also
    # redirect the standard handles by setting stdin, stdout or stderr.
    process = subprocess.Popen( command,
                                shell=True,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                close_fds=False )
If close_fds is true, all file descriptors except 0, 1 and 2 will be closed before the child process is executed. (Unix only). Or, on Windows, if close_fds is true then no handles will be inherited by the child process. Note that on Windows, you cannot set close_fds to true and also redirect the standard handles by setting stdin, stdout or stderr.
    output = process.stdout.read()[:-1]
    ret = process.wait()

    if ret != 0:
        sys.exit( "\nERROR: The command \n\n%s\n\nreturned a nonzero " \
                  "exit status. The error message is \n\n%s\n\n" \
                  "Abort.\n" % \
                  ( command, process.stderr.read()[:-1] )
                )
    return output
# ==============================================================================
def is_valid_font_file( filename ):
    """"
    Check if a file is actually a valid font file:
    make sure otfinfo returns a 0 exit code.
    """
    cmd = 'otfinfo --info %s' % filename

    process = subprocess.Popen( cmd,
                                shell=True,
                                stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                close_fds=False )
    if process.wait() == 0:
        # exit code 0: everything all right
        return True
    else:
        # exit code nonzero: something went wrong; probably not a font file
        return False
# ==============================================================================
# http://www.microsoft.com/typography/otspec/recom.htm
# We recommend using name ID's 8-12, to identify manufacturer,
# designer, description, URL of the vendor, and URL of the designer.
# URL's must contain the protocol of the site: for example, http:// or
# mailto: or ftp://. The OpenType font properties extension can
# enumerate this information to the users.
#
# The Subfamily string in the 'name' table should be used for variants
# of weight (ultra light to extra black) and style (oblique/italic or
# not). So, for example, the full font name of "Helvetica Narrow
# Italic" should be defined as Family name "Helvetica Narrow" and
# Subfamily "Italic". This is so that Windows can group the standard
# four weights of a font in a reasonable fashion for
# non-typographically aware applications which only support
# combinations of "bold" and "italic."
#
# The Full font name string usually contains a concatenation of
# strings 1 and 2. However, if the font is 'Regular' as indicated in
# string 2, then use only the family name contained in string 1. This
# is the font name that Windows will expose to users.
# ==============================================================================
def check_lcdf_version():
    """
    otfinfo requires version 2.38 or higher of otfinfo (which supports --info)
    """
    otfiv = run( "otfinfo --version" )

    otfinfoversion_re = re.compile("([0-9]+\.[0-9]+)")
    verstion_string = otfinfoversion_re.search( otfiv )
    if float(verstion_string.group(1)) < 2.38:
        sys.exit( "Your lcdf-typetools version is currently %s.\n" % \
                                                      verstion_string.group(1) +
                  "Please upgrade your lcdf-typetools installation " \
                  "to at least v2.38."  )
    return
# ==============================================================================
def user_warning( string ):
    """
    Display otfinst.py warning.
    """
    if OPTIONS.verbose:
        print "Warning! " + string + "\n"
    return
# ==============================================================================
# be non-greedy with the ':'
OTFINFO_RE = re.compile("^(.*?):\s+(.*)$")
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def get_font( fontfile ):
    """
    Given a font file, return a font object that structures all the info
    found in the font file.
    """
    # --------------------------------------------------------------------------
    # get the PostScript name
    #postScriptName = run( "otfinfo --postscript-name '%s'" % fontfile )
    # --------------------------------------------------------------------------
    # set default arguments
    font = { 'file name'         : fontfile,
             'weight'            : None,
             'width'             : None,
             'shape'             : None,
             'variant'           : None,
             'optical size'      : None,
             'optical size name' : None,
             'figure style'      : None
           }
    # --------------------------------------------------------------------------
    # set the fallback properties
    if OPTIONS.fallback_properties is not None:
        for fallback in OPTIONS.fallback_properties:
            font.update( process_subfamily( fallback ) )
    # --------------------------------------------------------------------------
    # get info on the font
    info = run( "otfinfo --info '%s'" % fontfile )
    # --------------------------------------------------------------------------
    # put the font info into a key-value list
    fontinfo = {}
    for line in info.split('\n'):
        otfinfo_line = OTFINFO_RE.search( line )
        if otfinfo_line:
            fontinfo[ otfinfo_line.group(1) ] = otfinfo_line.group(2)
    # --------------------------------------------------------------------------
    # translate the key-value list to fill font[postScriptName]

    # prefer 'Preferred family' over 'Family'
    if 'Preferred family' in fontinfo:
        font['family'] = fontinfo[ 'Preferred family' ].replace( " ", "" )
    elif 'Family' in fontinfo:
        font['family'] = fontinfo[ 'Family' ].replace( " ", "" )
    else:
        sys.exit( "\nCould not determine font family from otfinfo output. " \
                  "Abort.\n" )

    # prefer 'Preferred subfamily' over 'Subfamily'
    if 'Preferred subfamily' in fontinfo:
        font.update( process_subfamily( fontinfo[ 'Preferred subfamily' ] ) )
    elif 'Subfamily' in fontinfo:
        font.update( process_subfamily( fontinfo[ 'Subfamily' ] ) )
    else:
        sys.exit( "\nCould not determine font subfamily from otfinfo output. " \
                  "Abort.\n" )

    # get vendor code
    font['vendor'] = get_vendor( fontinfo )
    # --------------------------------------------------------------------------
    # get features
    font['features'] = get_features( fontfile )
    # --------------------------------------------------------------------------
    # get optical size
    optical_size = run( "otfinfo --optical-size '%s'" % fontfile )
    # --------------------------------------------------------------------------
    # process optical size
    # typical output:
    #     MinionPro-It.otf: design size 11 pt, size range (8.4 pt, 13 pt],
    #     subfamily ID 9, subfamily name Italic
    optical_re = re.compile( "size range \(([\d.]+) pt, ([\d.]+) pt\]" )
    optical_size_info = optical_re.search( optical_size )
    if optical_size_info:
        # convert from
        #   OTF's (exclusive, inclusive] to
        #   TeX's [inclusive, exclusive)
        # OTF measures in decipoints, so add 'eps' to each size
        eps = 0.0001
        font['optical size'] = [ float(optical_size_info.group(1)) + eps,
                                 float(optical_size_info.group(2)) + eps ]
    # --------------------------------------------------------------------------
    if OPTIONS.verbose:
        print prettyprint_font_info( font )
    # --------------------------------------------------------------------------
    return font
# ==============================================================================
def get_features( fontfile ):
    """
    Collects the features of a given font file.
    """
    features = []
    if has_kerning( fontfile ):
        features.append( "kern" )
    return features
# ==============================================================================
def has_kerning( fontfile ):
    """
    Checks if a font in a given file supports kerning.
    """
    # check the feature table
    features = run( "otfinfo --features '%s'" % fontfile )

    if re.search( "kern\tKerning", features ):
        return True
    else:
        # check the tables
        tables = run( "otfinfo --table '%s'" % fontfile )
        if re.search( "\w*\d+ kern", tables ):
            return True
        else:
            # Neither --features nor --table contains a hint that the font
            # might have kerning. Well, it probably hasn't, then. :)
            return False
    return
# ==============================================================================
def process_subfamily( subfamily_string ):
    """
    Parses the subfamily string, e.g.,
        "ItalicBold"     ---> ['italic','bold']
        "Roman"          ---> ['regular']
        "Extra Bold"     ---> ['extra bold']
        "ItalicBoldSmCp" ---> ['italic','bold','small caps']
    Then, return those in as a key-value list such as
      [ 'weight': 'light', 'width': 'condensed', 'shape': 'italic' ]
    """
    # --------------------------------------------------------------------------
    # First explode the string around the whitespaces and try to make sense of
    # the pieces.
    # Make sure
    properties_list = merge_font_properties( subfamily_string.split() )
    # --------------------------------------------------------------------------
    fontspecs = {}
    for prop in properties_list:
        # now, translate the result into width, weight, shape
        try:
            fontspecs.update( get_font_property( prop ) )
            continue
        except:
            pass

        # dissect by capitals, e.g., 'RegularThin' --> [ 'Regular', 'Thin' ]
        split_prop = merge_font_properties( re.findall('[A-Z][^A-Z]*', prop ) )
        for t in split_prop:
            try:
                fontspecs.update( get_font_property( t ) )
            except:
                user_warning( "Option '%s' not recognized. Discard." % prop )
    # --------------------------------------------------------------------------
    return fontspecs
# ==============================================================================
def merge_font_properties( prop_list ):
    """
    Takes a list [ 'Regular','Extra','thin' ] and makes sure that the 'extra'
    actually goes to 'thin', so returns
    [ 'Regular','Extrathin' ]
    """
    # combine 'extra' with next element, delete 'extra'
    for item in prop_list:
        if item.lower()=='extra' or item.lower()=='ultra':
            k = prop_list.index( item )
            prop_list[k+1] = item + prop_list[k+1]
            prop_list.remove( item )

    return prop_list
# ==============================================================================
def get_font_property( prop ):
    """
    Translate a font property string into a font specification.
    """
    if prop.lower()=='roman' or prop.lower()=='regular':
        return None
    elif prop.lower() in BERRY_WEIGHT_MAP:
        return { 'weight': prop.lower() }
    elif prop.lower() in BERRY_WIDTH_MAP:
        return { 'width': prop.lower() }
    elif prop.lower() in [ 'upright', 'roman', 'italic', 'oblique', 'slanted',
                        'upright italic', 'outline' ]:
        return { 'shape': prop.lower() }
    elif prop.lower() in [ 'smcp' ]:
        return { 'variant': prop.lower() }
    elif prop.lower() in [ 'caption', 'subhead', 'display' ]:
        # From <http://www.adobe.com/type/opentype/index.html#adv> :
        # Historically, some of the highest quality typefaces have included
        # different designs for different print sizes. Several of
        # Adobe's OpenType fonts include four optical size variations:
        # caption, regular, subhead and display. Called "Opticals," these
        # variations have been optimized for use at specific point sizes.
        # Although the exact intended sizes vary by family, the general size
        # ranges include:
        #     caption (6-8 point),
        #     regular (9-13 point),
        #     subhead (14-24 point) and
        #     display (25-72 point).
        return { 'optical size name': prop.lower() }
    elif prop.lower() in [ 'lf', 'osf', 'tlf', 'tosf' ]:
        return { 'figure style': prop.lower() }
    else:
        raise NameError
# ==============================================================================
URL_RE = re.compile("(\w*)\.(com|net|org|edu|de|fr|co\.uk|nl)")
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def get_vendor( fontinfo ):
    """
    Get vendor information of a fontinfo object.
    """

    if 'Vendor URL' in fontinfo:
        from urlparse import urlparse
        url = urlparse( fontinfo['Vendor URL'] )
        if URL_RE.search( url.netloc ):
            return URL_RE.search( url.netloc ).group(1)

    # fallback for vendor entry if none was found
    return  'generic'
# ==============================================================================
# From <http://www.ctan.org/tex-archive/info/Type1fonts/fontinstallationguide/>,
# p. 86.
TEX_WEIGHT_MAP = { 'ultralight' : 'ul',
                   'thin'       : 'ul',
                   'hairline'   : 'ul',
                   'extralight' : 'el',
                   'light'      : 'l',
                   'book'       : 'm',
                   'regular'    : 'm',
                   'roman'      : 'm',   # not in fontinstallationguide
                   None         : None,  # not in fontinstallationguide
                   'medium'     : 'mb',
                   'demi'       : 'db',  # not in fontinstallationguide
                   'demibold'   : 'db',
                   'semibold'   : 'sb',
                   'bold'       : 'b',
                   'heavy'      : 'eb',
                   'heavyface'  : 'eb',  # not in fontinstallationguide
                   'black'      : 'eb',
                   'extrabold'  : 'eb',
                   'extrablack' : 'eb',
                   'ultrabold'  : 'ub',
                   'ultrablack' : 'ub',
                   'poster'     : '--'
                 }

# From <http://www.ctan.org/tex-archive/info/Type1fonts/fontinstallationguide/>,
# p. 86.
TEX_WIDTH_MAP = { 'ultracompressed' : 'uc',
                  'ultracondensed'  : 'uc',
                  'extracompressed' : 'ec',
                  'extracondensed'  : 'ec',
                  'compressed'      : 'c',
                  'compact'         : 'c',  # not in fontinstallationguide
                  'condensed'       : 'c',
                  'cond'            : 'c',  # not in fontinstallationguide
                  'narrow'          : 'c',
                  'semicondensed'   : 'c',  # not in fontinstallationguide
                  'normal'          : None, # not in fontinstallationguide
                  'medium'          : None, # not in fontinstallationguide
                  'regular'         : None,
                  None              : None, # not in fontinstallationguide
                  'extended'        : 'x',
                  'elongated'       : 'x',  # not in fontinstallationguide
                  'semiexpanded'    : 'x',  # not in fontinstallationguide
                  'expanded'        : 'x',
                  'extraexpanded'   : 'ex',
                  'ultraexpanded'   : 'ux',
                  'wide'            : '--'
                }
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def get_tex_series( weight, width ):
    """
    A Berry series information consists of the concatenated keywords for
    weight and width (in that order). For example, a bold (b) extended (x)
    font has the series keyword 'bx'. If the weight is regular and width
    normal, then 'm' is returned.
    """
    try:
        tex_weight = TEX_WEIGHT_MAP[ weight ]
    except KeyError:
        sys.exit( "Weight '" + str(weight) + "' not found in TeX weight table."\
                  " Abort." )

    try:
        tex_width = TEX_WIDTH_MAP[ width ]
    except KeyError:
        sys.exit( "Width '" + str(width) + "' not found in TeX width table. " \
                  "Abort." )

    if tex_weight is None and tex_width is None:
        return 'm'
    elif tex_weight is None:
        return tex_width
    elif tex_width is None:
        return tex_weight
    else: # neither of the two is None
        return tex_weight + tex_width
# ==============================================================================
def get_tex_variant( font ):
    """
    Generates TeX variant from list of shape options.
    See <http://www.ctan.org/tex-archive/info/Type1fonts/fontinstallationguide/>.
    """
    if font['shape'] == 'italic' and font['variant'] == 'smcp':
        return 'si'
    elif font['shape'] == 'italic':
        return  'it'
    elif font['variant'] == 'smcp':
        return 'sc'
    elif font['shape'] == 'oblique' or font['shape'] == 'slanted':
        return 'sl'
    elif font['shape'] == 'upright italic':
        return  'ui'
    elif font['variant'] == 'outline':
        return 'ol'
    elif font['shape'] in  ['normal', 'upright', 'roman', None]:
        return 'n'
    else:
        sys.exit( "No legal TeX variant found for font\n" + str(font) + "\n" \
                  "Abort." )
# ==============================================================================
def generate_fd_and_sty( fontlist ):
    """
    Generates FD files for each font family and STY file for the typeface.
    """

    # Sort the entries by (font) family and then group.
    # Note that vendor, family, and figure style make up a TeX font family,
    # e.g., pag (Adobe Avantgarde) or pagj (Adobe Avantgarde with OsF) for
    # the Berry naming scheme.
    fontlist.sort( key=operator.itemgetter( 'vendor',
                                            'family',
                                            'figure style' ) )

    encoding_font_combinations = []

    for ( vendor_name, full_family_name ), fam_iter in \
          itertools.groupby( fontlist, operator.itemgetter('vendor','family') ):
        # ----------------------------------------------------------------------
        if OPTIONS.verbose:
            print "Processing font family '%s' of '%s'..." % \
                  ( vendor_name, full_family_name )
        # ----------------------------------------------------------------------
        # There's something that sty file and FD files share, namely the
        # TeX variable for font scaling. Define its name here, and pass it
        # to the file generating function.
        raw_tex_family_name = get_raw_tex_familyname( vendor_name,
                                                      full_family_name )
        scaling_variable_name = "%s@scale" % letterize( raw_tex_family_name )
        # ----------------------------------------------------------------------
        # generate FD files and store which figure styles there are for
        # the latter generation the STY file
        figure_styles = []
        for figure_style, font_iter  in  \
             itertools.groupby( fam_iter, operator.itemgetter('figure style') ):
            figure_styles.append( figure_style )
            # family_name will be used as a variable throughout the FD files
            tex_family_name = get_tex_familyname( vendor_name,
                                                  full_family_name,
                                                  figure_style )

            # Store the list of fonts in the group separately, as the iterator
            # family[1] is volatile in the sense that once it's been run
            # over, there's no way to reset. This way, only one encoding
            # would be possible.
            # See
            #   http://diveintopython3.org/advanced-iterators.html
            font_list = list( font_iter )

            for encoding in OPTIONS.encodings:
                encoding_font_combinations += \
                                        generate_fd_file( encoding,
                                                          tex_family_name,
                                                          scaling_variable_name,
                                                          font_list )
        # ----------------------------------------------------------------------
        # generate LaTeX style (STY) file
        styname = full_family_name + ".sty"
        #styoptions = styfiles[fontbase]
        styoptions = []
        generate_sty_file( styname, styoptions, figure_styles,
                           raw_tex_family_name, scaling_variable_name )
        # ----------------------------------------------------------------------
        if OPTIONS.verbose:
            print
        # ----------------------------------------------------------------------
    return encoding_font_combinations
# ==============================================================================
def generate_fd_file( encoding, tex_family_name,
                      scaling_variable_name, fontlist ):
    """
    Generates a font definition file for a given font family in a given
    encoding.
    """

    # example file name: LY1pun.fd
    fdfile = encoding.upper() + tex_family_name + ".fd"
    if OPTIONS.verbose:
        print "Generating font definition file '" + fdfile + "'..."
    # --------------------------------------------------------------------------
    # Store a list with font / encoding combinations that will need to be
    # generated by otftotfm later on.
    encoding_font_combinations = []
    # --------------------------------------------------------------------------
    # open the file
    try:
        fddest = open( fdfile, 'w' )
    except IOError:
        sys.exit( "Unable to create file '%s'. Abort." % fdfile )
    # --------------------------------------------------------------------------
    # HEADER
    print >> fddest, ("%% Autogenerated by %s on %s" %
                      (sys.argv[0], datetime.date.today().strftime("%Y/%m/%d")))

    date = datetime.date.today().strftime("%Y/%m/%d")
    print >> fddest, "\ProvidesFile{%s}[%s font definitions for %s/%s]" % \
                     (fdfile, date, encoding, tex_family_name)
    print >> fddest
    # --------------------------------------------------------------------------
    # SCALING
    # Prototypical example of scaling from Helvetica:
    #
    # \expandafter\ifx\csname Hv@scale\endcsname\relax
    #  \let\Hv@@scale\@empty
    # \else
    #  \edef\Hv@@scale{s*[\csname Hv@scale\endcsname]}%
    # \fi
    #
    # TODO This relies on the fact that scaling_variable_name contains the '@'
    #      character.
    internal_scaling_variable = "\\" + scaling_variable_name.replace('@','@@')
    print >> fddest, "\\expandafter\\ifx\\csname %s\\endcsname\\relax\n" \
                     "    \\let%s\\@empty\n" \
                     "\\else\n" \
                     "    \\edef%s{s*[\\csname %s\\endcsname]}%%\n" \
                     "\\fi" % ( scaling_variable_name,
                                internal_scaling_variable,
                                internal_scaling_variable,
                                scaling_variable_name )
    # --------------------------------------------------------------------------
    # declare font family
    print >> fddest, "\n\\DeclareFontFamily{%s}{%s}{}" \
                     % (encoding, tex_family_name)
    print >> fddest
    # --------------------------------------------------------------------------
    # Now, run through the function list and place a \DeclareFontShape entry
    # into the FD file.
    # To have all, e.g., 'bold' types together, first sort the list.
    # First sort along 'series', then 'shape'.

    # add TeX (NFSS) font properties
    tmp_fontlist = []
    for font in fontlist:
        font['TeX series']  = get_tex_series( font['weight'], font['width'] )
        font['TeX variant'] = get_tex_variant( font )
        tmp_fontlist.append( font )
    fontlist = tmp_fontlist

    # Bring them in the right order
    # and put all fonts with the same series and shape into a sublist.
    fontlist.sort( key=operator.itemgetter( 'TeX series', 'TeX variant' ) )
    series_shape_list = itertools.groupby( fontlist,
                            operator.itemgetter( 'TeX series', 'TeX variant' ) )

    # Loop over all groups with equal series/shape components, and place
    # a DeclareFontShape entry for them. For font without opticals, there will
    # likely be only one item in the group.
    series_shape_combinations = []
    for series_shape in series_shape_list:

        # Store series/shape combinations for later testing.
        # This is exactly the info that is contained in series_shape_list
        # already, but somehow this doesn't seem to persist.
        series_shape_combinations.append( series_shape[0] )

        print >> fddest, ( "\\DeclareFontShape{%s}{%s}{%s}{%s}{"%
                            ( encoding, tex_family_name,
                              series_shape[0][0], series_shape[0][1] )
                         ),

        # Now, handle all the optical size entries:
        # Order by optical size, and group. Each such group is of the structure
        #   ( [optical size specification], [list of corresponding fonts] ).
        # It has to be guaranteed that the font list conains only one entry;
        # otherwise, two TeX fonts would correspondond to the same NFSS entry,
        # i.e., same encoding, family, series, variant, opticals.
        fontlist = list(series_shape[1])
        fontlist.sort( key=operator.itemgetter( 'optical size' ) )
        opticals_list = itertools.groupby( fontlist,
                                         operator.itemgetter( 'optical size' ) )

        for (optical_size, optic_list) in opticals_list:
            if optical_size is not None:
                if len(optical_size) == 1:
                    # font laid out for a single size
                    opticals_specification = "<%s>" % str(optical_size)
                elif len(optical_size) == 2:
                    # font laid out for a size range
                    opticals_specification = "<%s-%s>" % \
                                    (str(optical_size[0]), str(optical_size[1]))
                else:
                    sys.exit( "Error: Illegal optical size item '" + \
                              str(optical_size) + "'. Abort.")
            else:
                # permit all sizes
                opticals_specification = "<->"

            print >> fddest, opticals_specification,

            # check if there is indeed not more than one font in the list
            optic_list = list( optic_list )
            if len(optic_list) > 1:
                msg = "\n\nError! The fonts/encoding combinations\n\n"
                for font in optic_list:
                    msg += "\tEncoding:\t" + encoding + "\n"
                    msg += "\t" + prettyprint_font_info( font ). \
                                  replace("\n","\n\t")
                    msg += "\n"
                msg += "could be resolved to the one TeX font specification\n\n"
                msg += "    \\DeclareFontShape{%s}{%s}{%s}{%s}{ %s }\n\n" % \
                                ( encoding, tex_family_name, series_shape[0][0],
                                  series_shape[0][1], opticals_specification )
                msg += "This would create serious ambiguities and yield " \
                       "unexpected results. Abort."
                sys.exit( msg )

            font = optic_list[0]

            # add scaling variable and TeX font name
            tex_font_name = get_tex_font_name( font, encoding )
            print >> fddest, "%s %s" % \
                             ( internal_scaling_variable, tex_font_name ),

            encoding_font_combinations.append( { "TeX font name": tex_font_name,
                                                 "encoding" : encoding,
                                                 "font" : font } )
        # ----------------------------------------------------------------------
        # close the DeclareFontShape entry
        print >> fddest, "}{}"
    # --------------------------------------------------------------------------
    # put empty line between proper definitions and substitutions
    print >> fddest
    # --------------------------------------------------------------------------
    # substitute slanted with italics
    for series_shape in series_shape_combinations:
        if series_shape[1] == 'it':
            # Is there an equivalent slanted font ('sl')?
            # If not, substitute.
            substituee_candidate = ( tex_family_name, series_shape[0], 'sl' )
            substitutor = (tex_family_name,) + series_shape
            check_and_substitute( fddest, encoding, series_shape_combinations,
                                  substituee_candidate, substitutor )

    # substitute italics with slanted
    for series_shape in series_shape_combinations:
        if series_shape[1] == 'sl':
            substituee_candidate = ( tex_family_name, series_shape[0], 'it' )
            substitutor = (tex_family_name,) + series_shape
            check_and_substitute( fddest, encoding, series_shape_combinations,
                                  substituee_candidate, substitutor )

    # substitute bold extended with bold
    for series_shape in series_shape_combinations:
        if series_shape[0] == 'b':
            substituee_candidate = ( tex_family_name, 'bx', series_shape[1] )
            substitutor = (tex_family_name,) + series_shape
            check_and_substitute( fddest, encoding, series_shape_combinations,
                                  substituee_candidate, substitutor )
            # substitutes bold extended slanted with bx/it if bx/sl
            # not available
            if series_shape[1] == 'it':
                substituee_candidate = ( tex_family_name, 'bx', 'sl' )
                substitutor = ( tex_family_name, 'bx', 'it' )
                check_and_substitute( fddest, encoding,
                                      series_shape_combinations,
                                      substituee_candidate, substitutor )
    # --------------------------------------------------------------------------
    print >> fddest, "\n\\endinput"
    fddest.close()
    return encoding_font_combinations
# ==============================================================================
def check_and_substitute( fddest, encoding, series_shape_combos,
                          substituee_candidate, substitutor ):
    """
    Finding items in lists is somewhat awkward in Python, there seems
    to be no default function for doing so. For now, use a generator
    function that will throw an exception on .next() if nothing
    was found.
    """
    try:
        # succeeds if something was found
        (i for i in series_shape_combos if i==substituee_candidate ).next()
    except StopIteration:
        # No success, not found. Substitute!
        print >> fddest, "\\DeclareFontShape{%s}{%s}{%s}{%s}{" % \
                         (( encoding, ) + substituee_candidate),
        # use silent substitution (ssub)
        print >> fddest, "<-> ssub * %s/%s/%s" % substitutor,
        print >> fddest, "}{}"
    return
# ==============================================================================
def generate_sty_file( filename, styoptions, figure_styles,
                       family_name, scaling_variable_name ):
    """
    Generate LaTeX style file.
    """
    # --------------------------------------------------------------------------
    if OPTIONS.verbose:
        print "Generating LaTeX style file '%s'..." % filename
    # --------------------------------------------------------------------------
    # open file
    try:
        stydest = open( filename, 'w' )
    except IOError:
        sys.exit( "Unable to create file '%s'. Abort." % filename )
    # --------------------------------------------------------------------------
    # header
    print >> stydest, ( "%% Autogenerated by %s on %s" %
                        ( sys.argv[0],
                          datetime.date.today().strftime("%Y/%m/%d") )
                      )
    print >> stydest, r"\NeedsTeXFormat{LaTeX2e}"

    basename = re.sub( "\.\w\w\w", "", filename )
    date = datetime.date.today().strftime("%Y/%m/%d")
    print >> stydest, r"\ProvidesPackage{%s}[%s %s support for LaTeX]" % \
                      (basename, date, family_name)
    # --------------------------------------------------------------------------
    # required packages
    print >> stydest, ""

    # encoding
    enc = []
    if "T1" in OPTIONS.encodings:
        enc.append( "T1" )
    if "LY1" in OPTIONS.encodings:
        enc.append( "LY1" )
    if enc:
        print >> stydest, r"\RequirePackage[%s]{fontenc}" % ",".join(enc)

    # The textcomp package provides commands such as
    #   \texteuro, \textyen, \textregistered, ... ,
    # if they are not provided already by LY1 or TS1.
    if 'LY1' not in OPTIONS.encodings and 'TS1' not in OPTIONS.encodings:
        print >> stydest, "\\RequirePackage{textcomp}"

    #print >> stydest, r"\RequirePackage{nfssext}"
    # --------------------------------------------------------------------------
    # if possible, pull in xkeyval
    print >> stydest, "\n" \
                      "\\IfFileExists{xkeyval.sty}{\n" \
                      "    \\newcommand*{\\%s}{1}\n" \
                      "    \\RequirePackage{xkeyval}\n" \
                      "    \\DeclareOptionX{scaled}{" \
                                            "\\renewcommand*{\\%s}{##1}}\n" \
                      "}{\n" \
                      "    \let\DeclareOptionX\DeclareOption\n" \
                      "    \let\ExecuteOptionsX\ExecuteOptions\n" \
                      "    \let\ProcessOptionsX\ProcessOptions\n" \
                      "}\n" % ( scaling_variable_name, scaling_variable_name )
    # --------------------------------------------------------------------------
    # superior and inferior figures
    seensups = 0
    seensinf = 0
    for option in styoptions:
        if 'sups' in option:
            seensups = 1
        if 'sinf' in option:
            seensinf = 1
    if seensups:
        print >> stydest, r"\def\@makefnmark{\hbox{\sustyle\@thefnmark}}"
        if seensinf:
            print >> stydest, r"\providecommand*{\textfrac}[2]{%"
            print >> stydest, r"  \textsu{#1}%"
            print >> stydest, r"  \textfractionsolidus"
            print >> stydest, r"  \textin{#2}}"
    # --------------------------------------------------------------------------
    # declare some variables used in this sty file
    lfamily_name = letterize( family_name )
    tex_text_family = r"\%s@Text@Family" % lfamily_name
    tex_math_family = r"\%s@Math@Family" % lfamily_name
    tex_text_figures = r"\%s@Text@Fig" % lfamily_name
    tex_math_figures = r"\%s@Math@Fig" % lfamily_name
    # --------------------------------------------------------------------------
    # set the TeX text and math families, e.g.,
    # \newcommand\Helvetica@Family{phv-\Helvetica@Fig}
    print >> stydest, r"\newcommand%s{%s%s}" % ( tex_text_family,
                                                 family_name,
                                                 tex_text_figures )
    print >> stydest, r"\newcommand%s{%s%s}" % ( tex_math_family,
                                                 family_name,
                                                 tex_math_figures )
    print >> stydest
    # --------------------------------------------------------------------------
    # handle the figure styles
    if len(figure_styles) > 1:
        # default style: append nothing (TODO adapt for long TeX names)
        if 'lf' in figure_styles:
            if OPTIONS.shortnames:
                appendix = FIGURESTYLE_APPENDIX_SHORT[ 'lf' ]
            else:
                appendix = FIGURESTYLE_APPENDIX_LONG[ 'lf' ]
            print >> stydest, r'\DeclareOptionX{textlf}{\def%s{%s}}' % \
                              ( tex_text_figures, appendix )
            print >> stydest, r'\DeclareOptionX{mathlf}{\def%s{%s}}' % \
                              ( tex_math_figures, appendix )
            print >> stydest, r'\DeclareOptionX{lf}{' \
                              r'\ExecuteOptionsX{textlf,mathlf}}'
            print >> stydest

        if 'osf' in figure_styles:
            if OPTIONS.shortnames:
                appendix = FIGURESTYLE_APPENDIX_SHORT[ 'osf' ]
            else:
                appendix = FIGURESTYLE_APPENDIX_LONG[ 'osf' ]
            print >> stydest, r'\DeclareOptionX{textosf}{\def%s{%s}}' % \
                              ( tex_text_figures, appendix )
            print >> stydest, r'\DeclareOptionX{mathosf}{\def%s{%s}}' % \
                              ( tex_math_figures, appendix )
            print >> stydest, r'\DeclareOptionX{osf}{' \
                              r'\ExecuteOptionsX{textosf,mathosf}}'
        print >> stydest
        # default: lf
        if 'lf' in figure_styles:
            print >> stydest, r'\ExecuteOptionsX{lf}'
        elif 'osf' in figure_styles:
            print >> stydest, r'\ExecuteOptionsX{osf}'
        else:
            sys.exit( "More than one figure style recognized, but not" \
                      "'lf' and 'osf'. Abort." )
        print >> stydest
    # --------------------------------------------------------------------------
    #onumprinted = False
    #lnumprinted = False
    ## change this if you don't want oldstyle to be the default
    #oldstyleisdefault = True
    #styledefaultprinted = False
    #numoptions = ''
    #for o in styoptions:
        #if not onumprinted and 'onum' in o:
            #if oldstyleisdefault == True:
                #print >> stydest, (r"\newcommand*{\%s@style}{%s}" %
                                  #(letterize(berryname[fontbase]),
                                    #optionMappings['onum']))
                #styledefaultprinted = True
            #numoptions += r"\DeclareOption{oldstyle}{%" + "\n"
            #numoptions += ( r"  \renewcommand*{\%s@style}{%s}%%" %
                            #( letterize(berryname[fontbase]),
                               #optionMappings['onum'] )
                          #)
            #numoptions += "\n}\n"
            #onumprinted = True
        #if not lnumprinted and 'lnum' in o:
            #if oldstyleisdefault == False:
                #print >> stydest, (r"\newcommand*{\%s@style}{%s}" %
                                  #(letterize(berryname[fontbase]),
                                    #optionMappings['lnum']))
                #styledefaultprinted = True
            #numoptions += r"\DeclareOption{lining}{%" + "\n"
            #numoptions += (r"  \renewcommand*{\%s@style}{%s}%%" %
                          #(letterize(berryname[fontbase]),
                            #optionMappings['lnum']))
            #numoptions += "\n}\n"
            #lnumprinted = True
    #if not lnumprinted and not onumprinted:
      #optionMappingsfakenum = ''
      #print >> stydest, ( r"\newcommand*{\%s@style}{%s}" %
                          #( letterize(family_name),
                            #optionMappingsfakenum
                          #)
                        #)
    #print >> stydest, numoptions
    # --------------------------------------------------------------------------
    # add options for separately pulling in text and math support
    tex_text = '@%s@Text@' % lfamily_name
    tex_math = '@%s@Math@' % lfamily_name
    print >> stydest, r'\newif\if%s' % tex_text
    print >> stydest, r'\newif\if%s' % tex_math
    # default: both true
    print >> stydest, r'\%strue' % tex_text
    print >> stydest, r'\%strue' % tex_math
    # options: onlytext, onlymath
    print >> stydest, r'\DeclareOptionX{onlytext}{\%strue\%sfalse}' % \
                      ( tex_text, tex_math )
    print >> stydest, r'\DeclareOptionX{onlymath}{\%sfalse\%strue}' % \
                      ( tex_text, tex_math )
    print >> stydest
    # --------------------------------------------------------------------------
    # process the options
    # TODO Provide starred version here?
    #      This would consider the options given to the *class* itself as well.
    print >> stydest, "\\ProcessOptionsX\\relax\n"
    # --------------------------------------------------------------------------
    # By default, use the installed font as rm;
    # the STY file accepts options 'rm', 'sf', 'tt' as well to make the font
    # default for the respective series.
    if OPTIONS.tex_family not in ['rm', 'sf', 'tt']:
        sys.exit( "Illegal TeX family '%s' provided. Choose either one of "
                  "'rm', 'sf', 'tt'. Abort." )

    print >> stydest, r"\if%s" % tex_text
    print >> stydest, r"  \renewcommand*{\%sdefault}{%s}" % \
                       ( OPTIONS.tex_family, tex_text_family )
    print >> stydest, r"\fi"
    print >> stydest
    # --------------------------------------------------------------------------
    # add optional math support
    print >> stydest, r'\if%s' % tex_math
    tex_math_letter_shape = r'\%s@Math@LetterShape' % lfamily_name
    print >> stydest, r'  \newcommand%s{it}' % tex_math_letter_shape

    # TODO better pull in OML encoded stuff here?
    print >> stydest, r'  \DeclareSymbolFont{operators}{OT1}{%s}{m} {n}' % \
                      tex_math_family
    print >> stydest, r'  \DeclareSymbolFont{letters}  {T1} {%s}{m} {%s}' % \
                      (tex_math_family, tex_math_letter_shape)
    print >> stydest, r'  \SetSymbolFont{letters}{bold}{T1} {%s}{bx}{%s}' % \
                      (tex_math_family, tex_math_letter_shape)
    print >> stydest, r'  \DeclareMathAlphabet\mathbf  {T1} {%s}{bx}{n}' % \
                      tex_math_family
    print >> stydest, r'  \DeclareMathAlphabet\mathit  {T1} {%s}{m} {it}' % \
                      tex_math_family
    print >> stydest, r'  \SetMathAlphabet\mathit{bold}{T1} {%s}{bx}{it}' % \
                      tex_math_family
    print >> stydest, r'  \DeclareMathAccent{\grave}   ' \
                      r'{\mathalpha}{operators}{0}'
    print >> stydest, r'  \DeclareMathAccent{\acute}   ' \
                      r'{\mathalpha}{operators}{1}'
    print >> stydest, r'  \DeclareMathAccent{\hat}     ' \
                      r'{\mathalpha}{operators}{2}'
    print >> stydest, r'  \DeclareMathAccent{\tilde}   ' \
                      r'{\mathalpha}{operators}{3}'
    print >> stydest, r'  \DeclareMathAccent{\ddot}    ' \
                      r'{\mathalpha}{operators}{4}'
    print >> stydest, r'  \DeclareMathAccent{\mathring}' \
                      r'{\mathalpha}{operators}{6}'
    print >> stydest, r'  \DeclareMathAccent{\check}   ' \
                      r'{\mathalpha}{operators}{7}'
    print >> stydest, r'  \DeclareMathAccent{\breve}   ' \
                      r'{\mathalpha}{operators}{8}'
    print >> stydest, r'  \DeclareMathAccent{\bar}     ' \
                      r'{\mathalpha}{operators}{9}'
    print >> stydest, r'  \DeclareMathAccent{\dot}     ' \
                      r'{\mathalpha}{operators}{10}'
    print >> stydest, r'\fi'
    print >> stydest
    # --------------------------------------------------------------------------
    print >> stydest, "\\endinput"
    # --------------------------------------------------------------------------
    stydest.close()
    return
# ==============================================================================
def assert_unique_tex_font_names( encoding_font_combinations ):
    """
    Checks if there are duplicate TeX font names in encoding_font_combinations
    and bails out if so.
    """
    efc = sorted( encoding_font_combinations,
                  key = operator.itemgetter('TeX font name') )

    tex_font_list = itertools.groupby( efc,
                                       operator.itemgetter('TeX font name') )

    # the list tex_font_list should not contain items with more than one subitem
    for item in tex_font_list:
        tex_font_name = item[0]
        font_encodings = list(item[1])
        if len(font_encodings) > 1:
            msg = "\n\nError! The fonts/encoding combinations\n\n"
            for font_encoding in font_encodings:
                msg += "\tEncoding:\t" + font_encoding['encoding'] + "\n"
                msg += "\t" + prettyprint_font_info( font_encoding['font'] ) \
                              .replace("\n","\n\t")
                msg += "\n"
            msg += "could be resolved to the *one* TeX font name '" + \
                   tex_font_name + "'.\n"
            msg += "This would create serious ambiguities and yield " \
                   "unexpected results. Abort."
            sys.exit( msg )

    return
# ==============================================================================
ENCODINGS = { 'OT1': { 'name'         : 'TeXText',
                       'berryencoding': '7t',
                       'encoding file': 'fontools_ot1',
                       'coding scheme': "TEX TEXT" },
              'TS1': { 'name'         : 'TeXTextCompanion',
                       'berryencoding': '8c',
                       'encoding file': 'fontools_ts1',
                       'coding scheme': "TEX TEXT COMPANION SYMBOLS 1---TS1" },
              'T1':  { 'name'         : 'Cork',
                       'berryencoding': '8t',
                       'encoding file': 'fontools_t1',
                       'coding scheme': "EXTENDED TEX FONT ENCODING - LATIN" },
              'LY1': { 'name'         : 'TeXnANSI',
                       'berryencoding': '8y',
                       'encoding file': 'fontools_ly1', # or texnansx.enc?
                       'coding scheme': "TEX TYPEWRITER AND WINDOWS ANSI" }
            }
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
def execute_commands( encoding_font_combinations, dryrun ):
    """
    Actually executes the otftotfm commands compiled previously.
    One example command line:
    $ otftotfm -a -e texnansx --no-create /path/to/ACaslonPro-Regular.otf \
                              -fkern -fliga LY1--AdobeCaslonPro-Regular
    """

    # Trust that the list encoding_font_combinations is already somewhat
    # ordered; this only affects the ordering the MAP file anyway.

    for font_encoding in encoding_font_combinations:
        # ----------------------------------------------------------------------
        # shortcut a few variables
        font = font_encoding['font']
        encoding = font_encoding['encoding']
        tex_font_name = font_encoding[ 'TeX font name' ]

        vendor_name = font[ 'vendor' ]
        family_name = font[ 'family' ]

        map_file = family_name.replace(" ","") + ".map"
        # ----------------------------------------------------------------------
        # gather the command line options
        otftotfm_options = [ "--no-updmap",
                             "--automatic",
                             "--no-type1",   # don't use Type 1
                                             # for OTF of TTF fonts
                             "--encoding='%s'" % \
                                      ENCODINGS[encoding]['encoding file'],
                             "--coding-scheme='%s'" % \
                                      ENCODINGS[encoding]['coding scheme'],
                             "--map-file='%s'" % map_file,
                             "--vendor='%s'" % vendor_name,
                             "--typeface='%s'" % \
                                        family_name.replace(" ","").lower()
                           ]

        if OPTIONS.extraoptions is not None:
            otftotfm_options.append( OPTIONS.extraoptions )

        if "kern" in font['features']:
            otftotfm_options.append( "--feature=kern" )

        if "liga" in font['features']:
            otftotfm_options.append( "--feature=liga" )
        else:
            # Try to manually pull in f-ligatures.
            # This works well for, e.g., TTF fonts that don't announce their
            # ligature capabilities.
            otftotfm_options.append( '--ligkern="f i =: fi"' )
            otftotfm_options.append( '--ligkern="f l =: fl"' )
            otftotfm_options.append( '--ligkern="f f =: ff"' )
            otftotfm_options.append( '--ligkern="ff i =: ffi"' )
            otftotfm_options.append( '--ligkern="ff l =: ffi"' )
        # ----------------------------------------------------------------------
        # assemble and run the command line
        command = "otftotfm %s '%s' '%s'\n" % ( " ".join( otftotfm_options ),
                                            font['file name'],
                                            tex_font_name )
        if not dryrun:
            run( command )
        # ----------------------------------------------------------------------
    # typically these two need to be called at the end to clean up
    if not dryrun and OPTIONS.verbose:
        print "\n\nDo not forget to 'texhash' and 'updmap --syncwithtrees' " \
              "before you try to use the font.\n\n"
# ==============================================================================
# ##############################################################################
# call the main function -- see the top of this file
if __name__ == '__main__':
    STATUS = main()
    sys.exit( STATUS )
# ##############################################################################
